"""
Streams -- Layer 1
Arbitrary, byte-oriented, unbuffered data sources/sinks
"""
import os
from cStringIO import StringIO as _StringIO
from tempfile import mkstemp as _mkstemp
from urllib2 import urlopen as _urlopen
from select import select as _select



#===============================================================================
# exceptions
#===============================================================================
class StreamError(Exception):
    pass
class OperationNotSupported(StreamError):
    pass
class StreamClosedError(StreamError):
    pass


#===============================================================================
# base streams
#===============================================================================
class Stream(object):
    """abstract base stream"""
    __slots__ = []
    
    def __init__(self):
        raise NotImplementedError
    def __del__(self):
        self.close()
    
    #
    # basic operations
    #
    def close(self):
        raise NotImplementedError
    
    def read(self, count = -1):
        """reads up to `count` bytes. if count is negative, reads up to end of
        the stream. new code should use readall() for this behavior. if reading
        at EOF, raises EOFError"""
        if count == 0:
            return ""
        if count < 0:
            return self.readall()
        return self._read(count)
    
    def _read(self, count):
        raise OperationNotSupported
    
    def readall(self):
        """reads up to the end of the stream; at EOF, returns an empty string"""
        raise OperationNotSupported
    
    def write(self, data):
        """writes the given data entirely"""
        raise OperationNotSupported
    
    #
    # introspection
    #
    def _get_closed(self):
        raise NotImplementedError
    closed = property(lambda self: self._get_closed(), 
        doc = "indicates whether the stream is closed")
    
    def _get_readable(self): 
        return False
    readable = property(lambda self: self._get_readable(), 
        doc = "indicates whether the stream is readable")
    
    def _get_writable(self): 
        return False
    writable = property(lambda self: self._get_writable(),
        doc = "indicates whether the stream is writable")
    
    def _get_seekable(self): 
        return isinstance(self, SeekableMixin)
    seekable = property(lambda self: self._get_seekable(),
        doc = "indicates whether the stream supports seek()ing and tell()ing")


class SeekableMixin(object):
    """a mixin implementing seek()ing and tell()ing APIs"""
    __slots__ = []
    END = None
    
    def seek(self, offset, whence = 0):
        """old-style seek; new code should use seekto, seekby, or seekend, or
        preferably the position property"""
        if whence == 0:
            self.seekto(offset)
        elif whence == 1:
            self.seekby(offset)
        elif whence == 2:
            self.seekend(offset)
        else:
            raise ValueError("invalid `whence`, must be 0, 1, or 2")
    
    def seekto(self, offset):
        """sets the stream's position relative to the start of the stream"""
        raise OperationNotSupported
    def seekby(self, offset):
        """sets the stream's position relative to the current position"""
        raise OperationNotSupported
    def seekend(self, offset):
        """sets the stream's position relative to the end of the stream"""
        raise OperationNotSupported
    def tell(self):
        """old-style tell; new code should use the position property"""
        raise OperationNotSupported        
    
    def _get_position(self):
        return self.tell()
    def _set_position(self, value):
        if value is self.END:
            self.seekend(0)
        elif value >= 0:
            self.seekto(value)
        else: # value < 0
            self.seekend(value)
    position = property(_get_position, _set_position, doc = 
        """gets of sets the stream's position. when setting, negative values
        seek relatively to the _end_ of the stream; positive (and zero) seek 
        relatively to the _start_ of the stream; specify s.position = s.END for
        the very end of the stream""") 


class OSStream(Stream):
    __slots__ = []
    def __repr__(self):
        return "<%s(%s)>" % (self.__class__.__name__, self.fileno())
    def fileno(self):
        """returns the file descriptor of the underlying OS resource"""
        raise NotImplementedError
    def sync(self):
        """synchronizes ('flushes') the file descriptor"""
        pass
    def _get_isatty(self):
        return os.isatty(self.fileno())
    isatty = property(lambda self: self._get_isatty(), 
        doc = "indicates whether this stream is attached to a TTY device")


class closed_stream(object):
    """a mockup 'closed' stream"""
    __slots__ = []
    def __repr__(self):
        return "<closed>"
    def close(self):
        pass
    def __int__(self):
        raise StreamClosedError
    def __getattr__(self, name):
        raise StreamClosedError
    fileno = __int__
    closed = property(lambda self: True)

closed_stream = closed_stream()


#===============================================================================
# OS-agnostic streams
#===============================================================================
class RandomStream(Stream):
    """a stream of random data"""
    __slots__ = ["_closed"]
    
    def __init__(self):
        self._closed = False
    def close(self):
        self._closed = True
    
    def _get_closed(self):
        return self._closed
    def _get_readable(self): 
        return True
    
    def _read(self, count):
        if self._closed: 
            raise StreamClosedError
        return "".join(chr(random.randint(0, 255)) for i in xrange(count))


class MemoryStream(Stream, SeekableMixin):
    """an in-memory stream of variable size (like StringIO)"""
    __slots__ = ["_stream"]
    
    def __init__(self):
        self._stream = _StringIO()
    
    def close(self):
        self._stream.close()
        self._stream = closed_stream
    
    def _get_closed(self):
        return self._stream is closed_stream
    def _get_readable(self): 
        return True
    def _get_writable(self):
        return True

    def tell(self):
        return self._stream.tell()
    def seekto(self, offset):
        self._stream.seek(offset, 0)
    def seekby(self, offset):
        self._stream.seek(offset, 1)
    def seekend(self, offset):
        self._stream.seek(offset, 2)
    
    def _read(self, count):
        return self._stream.read(count)
    def readall(self):
        return self._stream.read()
    def write(self, data):
        self._stream.write(data)
    
    def getdata(self):
        return self._stream.getvalue()


class ReadonlyMemoryStream(MemoryStream):
    def __init__(self, data):
        self._stream = _StringIO(data)
    def _get_writable(self):
        return False
    def write(self, data):
        raise OperationNotSupported


#===============================================================================
# OS streams
#===============================================================================
MAX_OS_READ_CHUNK = 16300

class FileStream(OSStream, SeekableMixin):
    __slots__ = ["_filename", "_mode", "_fd", "_abspath"]
    _filemodes = {
        "r" : os.O_RDONLY,
        "w" : os.O_CREAT | os.O_WRONLY | os.O_TRUNC,
        "a" : os.O_CREAT | os.O_APPEND | os.O_WRONLY,
        "r+" : os.O_RDWR,
        "w+" : os.O_CREAT | os.O_RDWR | os.O_TRUNC,
        "a+" : os.O_APPEND | os.O_RDWR,
    }
    
    def __init__(self, filename, mode, inheritable = True, exclusive = False):
        self._fd = closed_stream
        self._mode = mode
        self._filename = filename
        fd, abspath = self._open(filename, mode, inheritable, exclusive)
        self._fd = fd
        self._abspath = abspath
    
    def __repr__(self):
        return "<%s %r (%d)>" % (self.__class__.__name__, 
            self._filename, self._fd)
    
    @classmethod
    def from_fd(cls, filename, mode, fd):
        self = cls.__new__(cls)
        self._filename = filename
        self._mode = mode
        self._fd = fd
        return self
    
    @classmethod
    def _open(cls, filename, mode, inheritable, exclusive):
        if mode not in cls._filemodes:
            keys = sorted(cls._filemodes.keys())
            raise ValueError("`mode` must be one of %r, not %r" % (keys, mode))
        mode = cls._filemodes[mode]
        if not inheritable: 
            mode |= getattr(os, "O_NOINHERIT", 0)
        if exclusive: 
            mode |= getattr(os, "O_EXCL", 0)
        return os.open(filename, mode), os.path.abspath(filename)
    
    def close(self):
        if not self.closed:
            os.close(self._fd)
            self._fd = closed_stream
    def fileno(self):
        return self._fd
    
    def _get_closed(self):
        return self._fd is closed_stream
    
    def dup(self):
        return self.from_fd(os.dup(self._fd))
    def sync(self):
        os.fsync(self._fd)
    def stat(self):
        return os.fstat(self._fd)
    
    def seekto(self, offset):
        os.lseek(self._fd, offset, 0)
    def seekby(self, offset):
        os.lseek(self._fd, offset, 1)
    def seekend(self, offset):
        os.lseek(self._fd, offset, 2)
    def tell(self):
        return os.lseek(self._fd, 0, 1)
    
    def _get_readable(self): 
        return self._mode in ("r", "r+", "w+", "a+")
    def _get_writable(self):
        return self._mode in ("w", "a", "r+", "w+", "a+")    
    def _get_size(self):
        return self.stat().st_size
    def _set_size(self, size):
        if size < 0:
            raise ValueError("size must be >= 0")
        # TODO: implement this with ctypes/C-extension
        raise OperationNotSupported
    size = property(_get_size, _set_size, doc = "gets or sets the file's size")
    
    mode = property(lambda self: self._mode)
    
    def _read(self, count):
        buf = os.read(self._fd, max(count, MAX_OS_READ_CHUNK))
        if not buf:
            raise EOFError
        return buf
    
    def readall(self):
        data = []
        while True:
            buf = os.read(self._fd, MAX_OS_READ_CHUNK)
            if not buf:
                break
            data.append(buf)
        return "".join(data)
    
    def write(self, data):
        while data:
            cnt = os.write(self._fd, data)
            data = data[cnt:]


class TempFileStream(FileStream):
    """a stream over a temporary file; the file is removed when closed"""
    __slots__ = []
    
    def __init__(self):
        self._fd, self._filename = _mkstemp(text = False)
        self._mode = "w+"
    
    def close(self):
        if not self.closed:
            FileStream.close(self)
            os.remove(self._filename)


class PipeStream(OSStream):
    """a simplex stream over a pipe"""
    __slots__ = []
    
    def __init__(self, pipe):
        pass


class DualPipeStream(OSStream):
    """a duplex stream over two (simplex) pipes"""
    __slots__ = []
    
    def __init__(self, inpipe, outpipe):
        pass


class SocketStream(OSStream):
    """a stream over a socket"""
    __slots__= ["_sock"]
    POSINF = 1e100000000
    
    def __init__(self, sock):
        self._sock = sock
    
    @classmethod
    def create(cls, host, sockargs = ()):
        from socket import socket
        s = socket(*sockargs)
        s.connect(host)
        return cls(s)
    
    def close(self):
        self._sock.close()
        self._sock = closed_stream
    
    def fileno(self):
        return self._sock.fileno()
    
    def _get_closed(self):
        return self._sock is closed_stream
    
    def _get_readable(self):
        # TODO: take care of sock.shutdown() - only with sock2?
        return True
    
    def _get_writable(self):
        # TODO: take care of sock.shutdown() - only with sock2?
        return True
    
    def _get_may_read(self):
        return bool(_select([self._sock.fileno()], [], [], 0)[0])
    may_read = property(_get_may_read)
    
    def _get_may_write(self):
        return bool(_select([], [self._sock.fileno()], [], 0)[1])
    may_write = property(_get_may_write)
    
    def _read(self, count):
        buf = self._sock.recv(max(count, MAX_OS_READ_CHUNK))
        if not buf:
            raise EOFError
        return buf
    
    def readall(self):
        data = []
        while True:
            buf = self._sock.recv(MAX_OS_READ_CHUNK)
            if not buf:
                raise EOFError
            data.append(buf)
        return "".join(data)
    
    def readavail(self, limit = POSINF):
        """reads the available data in the socket's queue, up to `limit`"""
        if limit == 0:
            return ""
        if limit < 0:
            raise ValueError("`limit` must be >= 0")
        
        data = []
        while limit > 0 and self.may_read:
            buf = self._sock.recv(max(limit, MAX_OS_READ_CHUNK))
            if not buf:
                break
            limit -= len(buf)
            data.append(buf)
        
        if not data:
            raise EOFError
        return "".join(data)
    
    def write(self, data):
        while data:
            cnt = self._sock.send(data)
            data = data[cnt:]


class UrlStream(OSStream):
    """a stream to a URL path (readonly)"""
    __slots__ = ["_file"]
    
    def __init__(self, urlpath):
        self._file = _urlopen(urlpath)
    
    def close(self):
        self._file.close()
        self._file = closed_stream
    def fileno(self):
        return self._file.fileno()
    
    def _get_closed(self):
        return self._file.closed
    def _get_readable(self):
        return True
    
    def _read(self, count):
        return self._file.read(count)
    def readall(self):
        return self._file.read()
    
    mimeinfo = property(lambda self: self._file.info(), 
        doc = "the MIME info of this resource")


#===============================================================================
# we also want to have all other device-specific streams... to name some:
# * ComStream (RS232, using pyserial)
# * LptStream (LPT, using pyparallel)
# * platform-agnostic devices, for example
#    * /dev/null, /dev/zero, /dev/full, etc. for all platforms
#    * DiskStream (on linux /dev/hda, on windows \\.\PhysicalDrive0, etc.)
#
# but these may be better left out for third-party extensions
#===============================================================================

def PhysicalDiskStream(index, mode):
    """a platform-independent wrapper for disk-device streams"""
    if os.name == "nt":
        devname = "//./PhysicalDrive%d" % (index,)
    elif os.name == "posix":
        diskletters = "abcdefghijklmnopqrstuvwxyz"
        devname = "/dev/hd%s" % (diskletters[index],)
    else:
        raise OSError("platform %r not supported" % (os.name,))
    return FileStream(devname, mode)


































    